# Copyright 2007, 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""commit - classes for committing to a repository

This module provides facilities to commit changes to a repository.

Classes:
EditorAction -- base class for commit editor actions
Mkdir        -- add a directory
Delete       -- delete a node

Functions:
Drive        -- create and drive a commit editor

"""

import sys

import gvn

import svn.delta
import svn.ra

from svn.core import SWIG_SVN_INVALID_REVNUM as SVN_INVALID_REVNUM


class EditorAction(object):
  """Base class for commit editor actions; does nothing.

  Instances are callable; see Arguments, below.

  Arguments:
  parent  -- parent baton
  path    -- path to edit
  editor  -- svn_delta_editor_t
  pool    -- memory pool
  """
  def DiagLog(self, format, *args):
    """Call gvn.DiagLog for an editor action, prepending the class name."""
    klass = type(self)
    return gvn.DiagLog(klass.__module__ + '.' + klass.__name__ + format, *args)


class Mkdir(EditorAction):
  """Return a baton for a newly created directory.

  Raises:
  SubversionException.apr_err==???
  ???

  """

  def __init__(self, revision):
    self.revision = revision

  def __call__(self, parent, path, editor, pool):
    args = (path, parent,
            None, SVN_INVALID_REVNUM, # copyfrom
            pool)
    self.DiagLog('(%d) => add_directory%s\n', self.revision, args)
    return editor.add_directory(*args)


class Delete(EditorAction):
  """Delete a node.

  Raises:
  SubversionException.apr_err==SVN_ERR_FS_NOT_DIRECTORY
  SubversionException.apr_err==SVN_ERR_RA_DAV_PATH_NOT_FOUND
  SubversionException.apr_err==???
  ???

  """

  def __init__(self, revision):
    self.revision = revision

  def __call__(self, parent, path, editor, pool):
    args = (path, self.revision, parent, pool)
    self.DiagLog('(%d) => delete_entry%s\n', self.revision, args)
    editor.delete_entry(*args)
    return None


def Drive(repo, revprops, revision, paths, action_cb,
          postfix_txdelta_cb=None, pool=None):
  """Create and drive a commit editor; return svn_commit_info_t .

  If any revprop values are text (e.g. for svn:log), it must be a
  utf-8 encoded str, not unicode.  Callers must encode themselves
  because this function cannot know which values should be text.  It
  could accept unicode *keys* and encode those, but since the caller
  must already encode values, it seems pointless.  See also
  gvn.repository.Revision.

  Arguments:
  repo        -- Repository object
  revprops    -- dict mapping utf-8 encode str (revprop name) to a str of
                 arbitrary binary data (revprop value)
  revision    -- base revision
  paths       -- paths to commit
  action_cb             -- required callback(path, pool) that returns
                           an EditorAction for a path from paths
  postfix_txdelta_cb    -- optional callback(editor, edit_baton, pool)
                           called just before close_edit (usually
                           PostfixTextDeltaManager.Transmit)
  pool        -- memory pool

  Raises:
  SubversionException.apr_err==SVN_ERR_FS_NOT_DIRECTORY
  SubversionException.apr_err==SVN_ERR_RA_DAV_PATH_NOT_FOUND
  SubversionException.apr_err==???
  ???

  """

  commit_info = [None]
  def commit_cb(_commit_info, pool):
    commit_info[0] = _commit_info
  (editor, edit_baton) = svn.ra.get_commit_editor3(repo.ra,
                                                   revprops,
                                                   commit_cb,
                                                   None,  # lock_tokens
                                                   False, # keep_locks
                                                   pool)
  try:
    # Hang onto dir batons in this stack, popping them out when we
    # leave a directory.  This is because svn (at least as of r31145
    # which is in 1.5.0) does not take a reference to the baton (how
    # could it?), it just borrows ours.
    dir_batons = []
    def driver_cb(parent, path, pool):
      dir_baton = action_cb(path, pool)(parent, path, editor, pool)
      try:
        (last_path, last_baton) = dir_batons[-1]
      except IndexError:
        last_path = None
      if last_path is not None and not gvn.util.IsChild(path, last_path):
        dir_batons.pop(-1)
      dir_batons.append((path, dir_baton))
      return dir_baton
    svn.delta.path_driver(editor, edit_baton, revision, paths,
                          driver_cb, pool)
    if callable(postfix_txdelta_cb):
      postfix_txdelta_cb(editor, edit_baton, pool)
    editor.close_edit(edit_baton, pool)
  except:
    # Save full exception info so we can completely ignore abort_edit errors.
    (exc_type, exc_val, exc_tb) = sys.exc_info()
    try:
      editor.abort_edit(edit_baton, pool)
    except:
      # We already have an exception in progress, not much we can do
      # about this.  TODO(epg): Should we at least print it, before
      # discarding it?  If that's too confusing, maybe only in diag.
      pass
    # I'm assured by my trusted Python guru friend that just 'raise e'
    # will do the right thing (i.e. cause the original traceback, not
    # a new one from this line) in Python 3.
    raise exc_type, exc_val, exc_tb

  return commit_info[0]
